package com.designPatterns.singleton;

/**
 * 单例模式:延迟加载双重校验,并非每次获取实例都要同步
 * 此处第一次校验的目的是,若实例已经存在,则不需要去加锁,直接取得实例就好,这就降低了锁对性能的影响
 * 第二次校验的目的是,若实例不存在才需要加锁
 * 情景模拟:线程A和B 若A第一次判断,实例为空, 但还没有执行到同步快,这时线程B已经执行完同步块并且已经为instance赋值,只是A的判断已经失效
 * 所以线程A进入同步块后会再次判断以保证,不会重复创建实例.
 *
 * 主要在于instance = new Singleton()这句，这并非是一个原子操作，事实上在 JVM 中这句话大概做了下面 3 件事情。
 * 1.给 instance 分配内存
 * 2.调用 Singleton 的构造函数来初始化成员变量
 * 3.将instance对象指向分配的内存空间（执行完这步 instance 就为非 null 了）
 * 但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的，最终的执行顺序可能是 1-2-3 也可能是 1-3-2。
 * 如果是后者，则在 3 执行完毕、2 未执行之前，被线程二抢占了，这时 instance 已经是非 null 了（但却没有初始化），
 * 所以线程二会直接返回 instance，然后使用，然后顺理成章地报错。
 * 我们只需要将 instance 变量声明成 volatile 就可以了。
 *  * Created by houjinxin on 16/3/5.
 */
public class SingletonLazyLoadDoubleCheck {

    /**
     * volatile 禁用JVM指令重排
     * 有些人认为使用 volatile 的原因是可见性，也就是可以保证线程在本地不会存有 instance 的副本，每次都是去主内存中读取,但其实是不对的。
     * 使用 volatile 的主要原因是其另一个特性：禁止指令重排序优化。也就是说，在 volatile 变量的赋值操作后面会有一个内存屏障（生成的汇编代码上），
     * 读操作不会被重排序到内存屏障之前。比如上面的例子，取操作必须在执行完 1-2-3 之后或者 1-3-2 之后，不存在执行到 1-3 然后取到值的情况。
     * 从「先行发生原则」的角度理解的话，就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作（这里的“后面”是时间上的先后顺序）。
     * 但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM （Java 内存模型）是存在缺陷的，
     * 即时将变量声明成 volatile 也不能完全避免重排序，主要是 volatile 变量前后的代码仍然存在重排序问题。
     * 这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复，所以在这之后才可以放心使用 volatile。
     */
    private volatile static SingletonLazyLoadDoubleCheck instance;

    private SingletonLazyLoadDoubleCheck(){
        System.out.println("我是双重校验懒加载的类");
    }

    public static SingletonLazyLoadDoubleCheck getInstance(){
        if(instance == null) {
            synchronized (SingletonLazyLoadDoubleCheck.class){
                if(instance == null) {
                    instance = new SingletonLazyLoadDoubleCheck();
                }
            }
        }
        return instance;
    }

    /**
     * 该方法用于测试懒加载是否成功
     */
    public static void doSomething(){
        System.out.println("干了一些事情");
    }
}